from typing import *
from pathlib import Path
from datetime import datetime
from math import floor
from abc import ABC,abstractmethod
import sys
import ramda as R
import game,protocol,conf,lang
from PyQt6.QtCore import *
from PyQt6.QtWidgets import *
from PyQt6.QtGui import *
from uxtheme import uxtheme_use,uxtheme_color,uxtheme_font

class Const:
    WINDOW_TITLE_TAIL = f"(Windows-x64) 20230206"
    WINDOW_ICON_PATH = Path("assets/icon.ico")
    FRAME_START_MS = 200
    ENGINESYS_REFRESH_ACTIVES_SPAC = 1.0
class ConstLayout:
    WINDOW_WIDTH_RATE = 0.7
    WINDOW_HEIGHT_RATE = 0.75
    BOX_LEFT_STRETCH = 7
    BOX_RIGHT_STRETCH = 4
    GAMEBOARD_SIZE_RATE = 0.96

# Interface for Extern Control
# Data I/O(Ux<->IO) and Event Notify(Ux->IO)
class IO(ABC):
    # for get/set
    @abstractmethod
    def now_game(me) -> game.Game: pass
    @abstractmethod
    def black_seconds(me) -> Tuple[int,int]: pass
    @abstractmethod
    def white_seconds(me) -> Tuple[int,int]: pass
    @abstractmethod
    def black_use_ai(me) -> bool: pass
    @abstractmethod
    def white_use_ai(me) -> bool: pass
    @abstractmethod
    def black_use_ai_set(me, use_ai:bool) -> None: pass
    @abstractmethod
    def white_use_ai_set(me, use_ai:bool) -> None: pass
    @abstractmethod
    def now_in_think(me) -> bool: pass
    @abstractmethod
    def now_use_ai(me) -> bool: pass
    @abstractmethod
    def now_engine_id(me) -> int: pass
    @abstractmethod
    def now_name(me) -> str: pass
    @abstractmethod
    def next_use_ai(me) -> bool: pass
    @abstractmethod
    def next_engine_id(me) -> int: pass
    @abstractmethod
    def next_name(me) -> str: pass
    #
    @abstractmethod
    def engine_conf(me, name:str) -> List[protocol.BOTH.ConfItem]: pass
    #
    @abstractmethod
    def n_engine(me) -> int: pass
    @abstractmethod
    def n_engine_usable(me) -> int: pass
    @abstractmethod
    def engine_usable_connents(me) -> List[protocol.Connection]: pass

    # for event
    @abstractmethod
    def on_command_newgame(me, rule_uid:str, second_step:int, second_all:int) -> None: pass
    @abstractmethod
    def on_command_reset(me) -> None: pass
    @abstractmethod
    def on_command_ai_step(me) -> None: pass
    @abstractmethod
    def on_command_undo(me) -> None: pass
    @abstractmethod
    def on_command_redo(me) -> None: pass
    @abstractmethod
    def on_board_move(me, h:int, v:int) -> None: pass
    @abstractmethod
    def on_ready(me) -> None: pass
    @abstractmethod
    def on_frame(me) -> None: pass

# Interface for Ux Present
class Root(ABC):
    # use for extern/widgets only:
    @abstractmethod
    def send(me, target:str, *args, **kwargs) -> None: pass

    # use for extern only:
    @abstractmethod
    def overtime(me, name:str) -> None: pass
    @abstractmethod
    def start(me, io:IO) -> None: pass

    # use for widgets only:
    @abstractmethod
    def need_update(me, name_by_overtime:str) -> bool: pass
    @abstractmethod
    def action_join(me, target_by_send:str, fn:Callable[...,None]) -> None: pass
    @abstractmethod
    def frame_join(me, fn:Callable[[],None]) -> None: pass
    @abstractmethod
    def io(me) -> IO: pass

def ux_expend_x(me:QWidget) -> QWidget:
    sp = me.sizePolicy()
    sp.setHorizontalPolicy(QSizePolicy.Policy.MinimumExpanding)
    me.setSizePolicy(sp)
    return me

def ux_expend_y(me:QWidget) -> QWidget:
    sp = me.sizePolicy()
    sp.setVerticalPolicy(QSizePolicy.Policy.MinimumExpanding)
    me.setSizePolicy(sp)
    return me

def ux_fix_layout(me:QLayout) -> QLayout:
    me.setSpacing(0)
    me.setContentsMargins(0,0,0,0)
    return me

def ux_row_layout(parent:QWidget=None) -> QVBoxLayout:
    return ux_fix_layout(QVBoxLayout(parent))

def ux_column_layout(parent:QWidget=None) -> QHBoxLayout:
    return ux_fix_layout(QHBoxLayout(parent))

def ux_style_widget(me:QWidget, id:str) -> QWidget:
    if isinstance(id,str):
        assert(len(id) > 0)
        me.setObjectName(id)
    return me

def ux_label(id:str, text:str, parent:QWidget=None) -> QLabel:
    me:QLabel = ux_style_widget(QLabel(parent), id)
    me.setText(text)
    return me

def ux_pushbutton(text:str, id:str=None, on_click:Callable[[],None]=None, parent:QWidget=None) -> QPushButton:
    me:QPushButton = ux_style_widget(QPushButton(parent), id)
    me.setText(text)
    me.setCursor(Qt.CursorShape.PointingHandCursor)
    if callable(on_click):
        me.clicked.connect(on_click)
    return me

def ux_checkbutton(text:str, id:str=None, on_change:Callable[[],None]=None, parent:QWidget=None) -> QCheckBox:
    me:QCheckBox = ux_style_widget(QCheckBox(text, parent), id)
    if callable(on_change):
        me.stateChanged.connect(on_change)
    return me

def popup_error(message:str) -> None:
    QMessageBox.critical(None, "ERROR", message)

class __OnFrame():
    @abstractmethod
    def on_frame(me) -> None:pass

class __GameBoard(QWidget,__OnFrame):
    def __init__(me, root:Root):
        super().__init__()
        me.reset_params(me.size())
        me.setMouseTracking(True)
        me.setCursor(Qt.CursorShape.ArrowCursor)
        me.__io = root.io()
        me.__root = root
        root.frame_join(me.on_frame)

    def reset_params(me, canvas_size:QSize) -> Self:
        me.__CANVAS_W = canvas_size.width()
        me.__CANVAS_H = canvas_size.height()
        # 棋盘在画布中的大小
        me.__SIZE = int(ConstLayout.GAMEBOARD_SIZE_RATE * min(me.__CANVAS_W, me.__CANVAS_H))
        # 棋盘在画布中的边界位置
        me.__TOP = (me.__CANVAS_H - me.__SIZE) // 2
        me.__BOTTOM = me.__TOP + me.__SIZE
        me.__LEFT = (me.__CANVAS_W - me.__SIZE) // 2
        me.__RIGHT = me.__LEFT + me.__SIZE
        me.__BOARD_RECT = QRect(me.__LEFT, me.__TOP, me.__SIZE, me.__SIZE)
        # 方格间距：将棋盘划分为16x16的方格，每个格子的大小
        me.__SPAC = me.__SIZE / (1 + game.BOARD_SIZE)
        me.__SPAC2 = me.__SPAC / 2
        # 星位大小
        me.__STAR_SIZE = me.__SPAC2 / 2
        # 棋子大小
        me.__PIECE_SIZE = int(me.__SPAC - 2)
        return me

    def paintEvent(me, e:QPaintEvent) -> None:
        d2d = QPainter(me)
        d2d.setRenderHints(QPainter.RenderHint.Antialiasing | QPainter.RenderHint.SmoothPixmapTransform)
        d2d.fillRect(e.rect(), uxtheme_color("gamecanvas-bg"))
        d2d.fillRect(me.__BOARD_RECT, uxtheme_color("gameboard-bg"))
        d2d.setPen(QPen(Qt.GlobalColor.black, 1))
        d2d.drawRect(me.__BOARD_RECT)
        d2d.setFont(uxtheme_font("gameboard-coord"))
        for i in range(game.BOARD_SIZE):
            pos = (1+i) * me.__SPAC
            # 横线
            d2d.drawLine(QPointF(me.__LEFT+me.__SPAC, me.__TOP+pos), QPointF(me.__RIGHT-me.__SPAC, me.__TOP+pos))
            # 竖线
            d2d.drawLine(QPointF(me.__LEFT+pos, me.__TOP+me.__SPAC), QPointF(me.__LEFT+pos, me.__BOTTOM-me.__SPAC))
            # 坐标
            d2d.save()
            d2d.setPen(uxtheme_color("gameboard-coord"))
            loctext_h = chr(ord('A')+i)
            d2d.drawText(QRectF(me.__LEFT+pos-me.__SPAC2, me.__TOP, me.__SPAC, me.__SPAC), Qt.AlignmentFlag.AlignCenter, loctext_h)
            d2d.drawText(QRectF(me.__LEFT+pos-me.__SPAC2, me.__BOTTOM-me.__SPAC, me.__SPAC, me.__SPAC), Qt.AlignmentFlag.AlignCenter, loctext_h)
            loctext_v = str(game.BOARD_SIZE - i)
            d2d.drawText(QRectF(me.__LEFT, me.__TOP+pos-me.__SPAC2, me.__SPAC, me.__SPAC), Qt.AlignmentFlag.AlignCenter, loctext_v)
            d2d.drawText(QRectF(me.__RIGHT-me.__SPAC, me.__TOP+pos-me.__SPAC2, me.__SPAC, me.__SPAC), Qt.AlignmentFlag.AlignCenter, loctext_v)
            d2d.restore()
            # 星位
            d2d.save()
            d2d.setBrush(uxtheme_color("gameboard-star"))
            for hv in game.STARS:
                x = me.__LEFT + ((1+hv[0]) * me.__SPAC) - (me.__STAR_SIZE / 2)
                y = me.__TOP + ((1+hv[1]) * me.__SPAC) - (me.__STAR_SIZE / 2)
                d2d.drawEllipse(QRectF(x, y, me.__STAR_SIZE, me.__STAR_SIZE))
            d2d.restore()
        # 棋子
        def draw_pieces(h:int, v:int, piece:int) -> None:
            if piece != game.P_EMPTY:
                x = me.__LEFT + ((h+1) * me.__SPAC) - (me.__PIECE_SIZE / 2)
                y = me.__TOP + ((v+1) * me.__SPAC) - (me.__PIECE_SIZE / 2)
                d2d.save()
                if piece == game.P_BLACK:
                    d2d.setPen(uxtheme_color("gamepiece-black-border"))
                    d2d.setBrush(uxtheme_color("gamepiece-black-bg"))
                else:
                    d2d.setPen(uxtheme_color("gamepiece-white-border"))
                    d2d.setBrush(uxtheme_color("gamepiece-white-bg"))
                d2d.drawEllipse(QRectF(x, y, me.__PIECE_SIZE, me.__PIECE_SIZE))
                d2d.restore()
        now_game = me.__io.now_game()
        now_game.board().for_hv(draw_pieces)
        # 步数
        d2d.setFont(uxtheme_font("gamestep"))
        for i in range(now_game.step_nums()):
            move = now_game.step_record(i)
            d2d.save()
            if move.piece() == game.P_BLACK:
                d2d.setPen(uxtheme_color("gamestep-black"))
            else:
                d2d.setPen(uxtheme_color("gamestep-white"))
            h,v = game.loc_to_hv(move.loc())
            x = me.__LEFT + ((h+1) * me.__SPAC) - (me.__PIECE_SIZE / 2)
            y = me.__TOP + ((v+1) * me.__SPAC) - (me.__PIECE_SIZE / 2)
            d2d.drawText(QRectF(x, y, me.__PIECE_SIZE, me.__PIECE_SIZE), Qt.AlignmentFlag.AlignCenter, str(move.step()))
            d2d.restore()

    def resizeEvent(me, e:QResizeEvent) -> None:
        if QSize(me.__CANVAS_W, me.__CANVAS_H) != e.size():
            me.reset_params(e.size())

    def __point_to_hv(me, point:QPointF):
        h = min(game.BOARD_SIZE-1, max(floor((point.x() - me.__LEFT - me.__SPAC2) / me.__SPAC), 0))
        v = min(game.BOARD_SIZE-1, max(floor((point.y() - me.__TOP - me.__SPAC2) / me.__SPAC), 0))
        return (int(h), int(v))

    def __mouse_in_board(me, point:QPointF) -> bool:
        return (point.x() in range(me.__LEFT, me.__RIGHT)) and (point.y() in range(me.__TOP, me.__BOTTOM))

    def mouseMoveEvent(me, e:QMouseEvent) -> None:
        if me.__mouse_in_board(e.position()):
            me.setCursor(Qt.CursorShape.PointingHandCursor)
        else:
            me.setCursor(Qt.CursorShape.ArrowCursor)
        super().mouseMoveEvent(e)

    def mousePressEvent(me, e:QMouseEvent) -> None:
        if me.__mouse_in_board(e.position()):
            h,v = me.__point_to_hv(e.position())
            me.__io.on_board_move(h,v)
        super().mousePressEvent(e)

    def on_frame(me) -> None:
        if me.__root.need_update("gameboard"):
            me.update()

class __GameStatus(QWidget,__OnFrame):
    def __init__(me, root:Root):
        super().__init__()
        box = ux_row_layout(me)
        # 对局时限：[第一行]
        row1 = ux_column_layout()
        row1.addWidget(ux_label(id="gamestatus-label", text=lang.path("gamestatus","label-maxtime")), 1)
        row1_right = ux_column_layout()
        me.__label_timer_black = ux_label(id="gamestatus-text", text=me.__timer_show(game.P_BLACK, (0,0)))
        row1_right.addWidget(me.__label_timer_black)
        me.__label_timer_white = ux_label(id="gamestatus-text", text=me.__timer_show(game.P_WHITE, (0,0)))
        row1_right.addWidget(me.__label_timer_white)
        row1.addLayout(row1_right, 5)
        box.addLayout(row1)
        # 对局状态：[第二行]
        row2 = ux_column_layout()
        row2.addWidget(ux_label(id="gamestatus-label", text=lang.path("gamestatus","label-pk")), 1)
        me.__label_pk = ux_label(id="gamestatus-text", text="")
        row2.addWidget(me.__label_pk, 5)
        box.addLayout(row2)
        # 引擎状态：[第三行]
        row3 = ux_column_layout()
        row3.addWidget(ux_label(id="gamestatus-label", text=lang.path("gamestatus","label-aistatus")), 1)
        me.__label_aistatus = ux_label(id="gamestatus-text", text="")
        row3.addWidget(me.__label_aistatus, 5)
        box.addLayout(row3)
        #
        me.__root = root
        me.__io = root.io()
        root.action_join("gamestatus/aistatus", (lambda a:me.__label_aistatus.setText(a)))
        root.frame_join(me.on_frame)

    def __timer_show(me, piece:int, seconds:Tuple[int,int]) -> str:
        def part(second:int) -> str:
            hour = (second // 3600)
            minute = ((hour % 3600) // 60) if (hour > 3599) else (second // 60)
            second = second % 60
            return f"{hour:02}:{minute:02}:{second:02}"
        second_step,second_all = seconds
        return f"{lang.piece_name(piece)}：{part(second_step)} / {part(second_all)}"

    def __on_frame_timer(me, now_game:game.Game) -> None:
        if now_game.has_over():
            me.__label_timer_black.setObjectName("gamestatus-text")
            me.__label_timer_white.setObjectName("gamestatus-text")
            me.__label_timer_black.setText(me.__timer_show(game.P_BLACK, (0,0)))
            me.__label_timer_white.setText(me.__timer_show(game.P_WHITE, (0,0)))
        else:
            if now_game.next_piece() == game.P_BLACK:
                me.__label_timer_black.setObjectName("gamestatus-text-timer-now")
                me.__label_timer_white.setObjectName("gamestatus-text")
            elif now_game.next_piece() == game.P_WHITE:
                me.__label_timer_black.setObjectName("gamestatus-text")
                me.__label_timer_white.setObjectName("gamestatus-text-timer-now")
            me.__label_timer_black.setStyle(me.__label_timer_black.style())
            me.__label_timer_white.setStyle(me.__label_timer_white.style())
            me.__label_timer_black.setText(me.__timer_show(game.P_BLACK, me.__io.black_seconds()))
            me.__label_timer_white.setText(me.__timer_show(game.P_WHITE, me.__io.white_seconds()))

    def __on_frame_pk(me, now_game:game.Game) -> None:
        if now_game.has_over():
            over_type = now_game.over_type()
            if over_type == game.GameOver.NORMAL:
                piece_name = lang.piece_name(now_game.last().piece())
                me.__label_pk.setText(lang.path("gamestatus","pk-over", WINNER=piece_name))
            elif over_type == game.GameOver.LONG:
                piece_name = lang.piece_name(now_game.last().piece())
                me.__label_pk.setText(lang.path("gamestatus","pk-over-long", WINNER=piece_name))
            elif over_type == game.GameOver.FORBIDDEN:
                me.__label_pk.setText(lang.path("gamestatus","pk-over-forbidden"))
            elif over_type == game.GameOver.DRAW:
                me.__label_pk.setText(lang.path("gamestatus","pk-over-draw"))
            elif over_type == game.GameOver.OVERTIME:
                winner = me.__io.next_name()
                loser = me.__io.now_name()
                me.__label_pk.setText(lang.path("gamestatus","pk-over-overtime", WINNER=winner, LOSER=loser))
        elif now_game.next_step() < 2:
            me.__label_pk.setText(lang.path("gamestatus","pk-wait-first", PIECE=lang.path("token","piece-black")))
        else:
            lastmove = now_game.last()
            step = lastmove.step()
            loctext = protocol.Const.LOC_TO_TEXT[lastmove.loc()]
            piece_name = lang.piece_name(lastmove.piece())
            shape = lang.shape_name(max(game.shape_line4(now_game.board(), lastmove.piece(), lastmove.loc())))
            me.__label_pk.setText(lang.path("gamestatus","pk-round", STEP=step, PIECE=piece_name, LOC=loctext, SHAPE=shape))

    def __on_frame_aistatus(me, now_game:game.Game) -> None:
        if me.__io.n_engine_usable() > 0:
            if now_game.has_over():
                me.__label_aistatus.setText(lang.path("gamestatus","aistatus-wait-new"))
            elif me.__io.now_in_think() and me.__io.now_use_ai():
                me.__label_aistatus.setText(lang.path("gamestatus","aistatus-in-think", ID=me.__io.now_engine_id()))
            elif me.__io.next_use_ai():
                engine_id = me.__io.next_engine_id()
                piece_name = me.__io.now_name()
                me.__label_aistatus.setText(lang.path("gamestatus","aistatus-wait-move", ID=engine_id, PIECE=piece_name))
            else:
                me.__label_aistatus.setText(lang.path("gamestatus","aistatus-ready", N=me.__io.n_engine_usable()))
        else:
            me.__label_aistatus.setText(lang.path("gamestatus","aistatus-no-any-usable"))

    def on_frame(me) -> None:
        now_game = me.__io.now_game()
        me.__on_frame_timer(now_game)
        me.__on_frame_aistatus(now_game)
        if me.__root.need_update("gameboard"):
            me.__on_frame_pk(now_game)

class __Tools(QWidget,__OnFrame):
    def __init__(me, root:Root):
        super().__init__()
        me.__io = root.io()
        box = ux_column_layout(me)
        box.addWidget(ux_pushbutton(text=lang.path("tools","reset"), on_click=me.__io.on_command_reset))
        me.__cb_ai_play_black = ux_checkbutton(text=lang.path("tools","ai-play-black"), on_change=me.on_cb_ai_play)
        box.addWidget(me.__cb_ai_play_black)
        me.__cb_ai_play_white = ux_checkbutton(text=lang.path("tools","ai-play-black"), on_change=me.on_cb_ai_play)
        box.addWidget(me.__cb_ai_play_white)
        box.addWidget(ux_pushbutton(text=lang.path("tools","ai-step"), on_click=me.__io.on_command_ai_step))
        box.addWidget(ux_pushbutton(text=lang.path("tools","undo"), on_click=me.__io.on_command_undo))
        box.addWidget(ux_pushbutton(text=lang.path("tools","redo"), on_click=me.__io.on_command_redo))
        #
        root.frame_join(me.on_frame)

    def on_cb_ai_play(me) -> None:
        if me.__io.black_use_ai() != me.__cb_ai_play_black.isChecked():
            me.__io.black_use_ai_set(me.__cb_ai_play_black.isChecked())
        if me.__io.white_use_ai() != me.__cb_ai_play_white.isChecked():
            me.__io.white_use_ai_set(me.__cb_ai_play_white.isChecked())

    def on_frame(me) -> None:
        if me.__cb_ai_play_black.isChecked() != me.__io.black_use_ai():
            me.__cb_ai_play_black.setChecked(me.__io.black_use_ai())
        if me.__cb_ai_play_white.isChecked() != me.__io.white_use_ai():
            me.__cb_ai_play_white.setChecked(me.__io.white_use_ai())

class _Tabs_Newgame(QWidget,__OnFrame):
    def __init__(me, root:Root):
        super().__init__()

    def on_frame(me) -> None:
        pass

class _Tabs_Log(QWidget,__OnFrame):
    def __init__(me, root:Root):
        super().__init__()
        box = ux_row_layout()
        button_clear = ux_pushbutton(id="tabs-log-clear", text=lang.path("tabs","log","label-clear"), on_click=me.action_clear)
        box.addWidget(button_clear)
        me.__text = QTextEdit()
        me.__text.setObjectName("tabs-log-text")
        me.__text.setReadOnly(True)
        me.__text.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
        me.__text.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
        me.__text.viewport().setCursor(Qt.CursorShape.ArrowCursor)
        box.addWidget(ux_expend_y(me.__text))
        me.setLayout(box)
        #
        root.action_join("log/clear", me.action_clear)
        root.action_join("log/push", me.action_push)

    def action_clear(me) -> None:
        me.__text.clear()

    def action_push(me, text:str) -> None:
        cur = me.__text.textCursor()
        cur.movePosition(QTextCursor.MoveOperation.Start)
        cur.insertText(f"{text.rstrip()}\n")

    def on_frame(me) -> None: pass

class _Tabs_Setting(QWidget,__OnFrame):
    def __init__(me, root:Root):
        super().__init__()

    def on_frame(me) -> None: pass

class _Tabs_Enginesys(QWidget,__OnFrame):
    def __init__(me, root:Root):
        super().__init__()

    def on_frame(me) -> None: pass

class _Tabs_Lib(QWidget,__OnFrame):
    def __init__(me, root:Root):
        super().__init__()

    def on_frame(me) -> None: pass

class _Tabs_About(QWidget,__OnFrame):
    def __init__(me, root:Root):
        super().__init__()

    def on_frame(me) -> None: pass

class __Tabs(QTabWidget):
    def __init__(me, root:Root):
        super().__init__()
        me.addTab(_Tabs_Newgame(root), lang.path("tabs","newgame","title"))
        select = me.addTab(_Tabs_Log(root), lang.path("tabs","log","title"))
        me.addTab(_Tabs_Setting(root), lang.path("tabs","setting","title"))
        me.addTab(_Tabs_Enginesys(root), lang.path("tabs","enginesys","title"))
        me.addTab(_Tabs_Lib(root), lang.path("tabs","lib","title"))
        me.addTab(_Tabs_About(root), lang.path("tabs","about","title"))
        me.setCurrentIndex(select)
        root.frame_join(me.on_frame)

    def on_frame(me) -> None:
        widget:__OnFrame = me.currentWidget()
        widget.on_frame()

def _ux_make_layout(window:QWidget, root:Root) -> None:
    box = ux_column_layout(window)
    #
    left = ux_row_layout()
    left.addWidget(__GameStatus(root))
    gameboard = __GameBoard(root)
    left.addWidget(ux_expend_y(gameboard))
    left.addWidget(__Tools(root))

    box.addLayout(left, ConstLayout.BOX_LEFT_STRETCH)
    #
    box.addWidget(__Tabs(root), ConstLayout.BOX_RIGHT_STRETCH)

class __Root(Root):
    def __init__(me, fps:int) -> None:
        me.__frame_fns = list()
        me.__last_frame_time = datetime.now()
        me.__io = None
        me.__action_map = dict()
        me.__overtimes = set()
        me.__qapp = QApplication(sys.argv)
        me.__frame_timer = QTimer()
        me.__frame_timer.setInterval(int(1000 / fps))

    def start(me, io:IO) -> None:
        try:
            me.__io = io
            me.__window = me.__make()
            QTimer.singleShot(Const.FRAME_START_MS, me.__on_frame_0)
            me.__qapp.exec()
            me.__frame_timer.stop()
        except Exception as e:
            popup_error(lang.path("err","ux-catch-except", MSG=e))
            raise(e)

    def raii(me) -> None:
        pass

    def send(me, target:str, *args, **kwargs) -> None:
        if target in me.__action_map:
            fn = me.__action_map.get(target)
            fn(*args, **kwargs)

    def overtime(me, name:str) -> None:
        me.__overtimes.add(name)

    def io(me) -> IO: return me.__io

    def need_update(me, name_by_overtime:str) -> bool:
        return (name_by_overtime in me.__overtimes)

    def action_join(me, target_by_send:str, fn:Callable[..., None]) -> None:
        if callable(fn):
            me.__action_map[target_by_send] = fn

    def frame_join(me, fn:Callable[[], None]) -> None:
        if callable(fn):
            me.__frame_fns.append(fn)

    def __on_frame(me) -> None:
        # Step-1 Notify Extern Processer
        me.__io.on_frame()
        # Step-2 Frame Processers
        R.for_each((lambda f:f()), me.__frame_fns)
        # Step-3 Reset Overtime-States
        me.__overtimes.clear()

    def __on_frame_0(me) -> None:
        me.__io.on_ready()
        me.__window.show()
        me.__last_frame_time = datetime.now()
        me.__frame_timer.timeout.connect(me.__on_frame)
        me.__frame_timer.start()

    def __make(me) -> QDialog:
        window = QDialog()
        window.setWindowFlag(Qt.WindowType.WindowMinimizeButtonHint, True)
        window.setWindowTitle(f"{lang.path('window', 'title-head')} {Const.WINDOW_TITLE_TAIL}")
        screen_rect = QGuiApplication.primaryScreen().geometry()
        width = int(ConstLayout.WINDOW_WIDTH_RATE * screen_rect.width())
        height = int(ConstLayout.WINDOW_HEIGHT_RATE * screen_rect.height())
        window.setFixedSize(width, height)
        window.setWindowIcon(QIcon(str(Const.WINDOW_ICON_PATH)))
        window.setMouseTracking(True)
        uxtheme_use(window)
        _ux_make_layout(window, me)
        return window

def make_root(fps:int) -> Root:
    return __Root(fps)
